<?php

require_once('include_config_mysql.php');
require_once('class_util.php');

// TODO: Document public function heavily.
// TODO: Try to figure out a way to allow arbitrary keys for mass operators. It
//    is easy enough to create a list of values in the checkboxes value field.
//    But I'm not sure how to get the dropdown menu to ask for the appropriate
//    value.

/**
 * ManagedTable
 *
 * @package 
 * @author Michael Thomas
 * @copyright 2008
 * @version 4.0
 * @access public
 */
class ManagedTable
{
  const PARAM_MAX_ROWS = 'r';
  const PARAM_PAGE_NUMBER = 'p';
  const PARAM_SORT_DIR = 'd';
  const PARAM_SORT_COL = 'c';
  const PARAM_SEARCH_KEY = 'k';
  const PARAM_SEARCH_VAL = 'v';
  const PARAM_SEARCH_MODE = 'm';

  const SORT_ASCENDING = 'ASC';
  const SORT_DESCENDING = 'DESC';

  const SEARCH_NORMAL = 0;
  const SEARCH_EXACT = 1;
  const SEARCH_GROUP = 2;

  const PAGINATION_LINKS = 12;
  const BUTTON_WIDTH = 33;
  const LEFT_MARGIN = 5;

  public $debug = false;

  protected $mysqlTable;
  protected $mysqlDatabase;
  protected $mysqlPrimaryKey;
  protected $mysqlStart = 1;
  protected $mysqlJoinedTables = array();
  protected $mysqlConstraints = array();
  protected $mysqlGroupBy; // TODO: Consider deprecating

  protected $tableColumns = array();
  protected $tableButtons = array();
  protected $tableData = array();
  protected $tableColumnNames = array();

  protected $searchEnabled = true;
  protected $searchKey;
  protected $searchValue;
  protected $searchColumns;
  protected $searchMode;

  protected $sortEnabled = true;
  protected $sortDirection = self::SORT_ASCENDING;
  protected $sortColumn;

  protected $maxRows = 50;
  protected $maxRowsOptions = array(10, 20, 50, 100, 250);

  protected $massOperatorsEnabled = false;
  protected $massOperators = array();
  protected $massOperatorsKey;

  protected $resultSize = 0;
  protected $pageNumber = 1;

  protected $defaultMaxRows;
  protected $defaultSearchColumn;
  protected $defaultSortDirection;
  protected $defaultSortColumn;

  protected $validSearchModes = array(self::SEARCH_NORMAL, self::SEARCH_EXACT,
      self::SEARCH_GROUP);
  protected $validSortDirections = array(self::SORT_ASCENDING,
      self::SORT_DESCENDING);

  protected $leftMarginWidth;

  /*
    The maximum number of buttons to show. More than this will be collapsed into
    a single drop-menu button.
  */
  protected $maxButtons = 2;

  /**
   * ManagedTable::ManagedTable()
   *
   * @param mixed $table
   * @param mixed $database
   * @return
   */
  public function __construct($mysqlTable, $mysqlPrimaryKey,
      $mysqlDatabase = null)
  {
    $this->mysqlTable = $mysqlTable;
    $this->mysqlPrimaryKey = $mysqlPrimaryKey;
    $this->mysqlDatabase = $mysqlDatabase;
  }

  /**
   * ManagedTable::loadColumnsFromMysql()
   *
   * @return
   */
  public function loadColumnsFromMysql()
  {
    // TODO: Test this.
    if (is_numeric(strpos($this->mysqlTable, ','))) {
      echo "Multiple tables aren't supported with getColumns()";
      exit;
    }
    if (isset($this->mysqlDatabase)) {
      $from = $this->mysqlDatabase . '.' . $this->mysqlTable;
    } else {
      $from = $this->mysqlTable;
    }
    $result = Util::query("SHOW COLUMNS FROM $from");
    while ($data = mysql_fetch_array($result, MYSQL_ASSOC)) {
      $this->addColumn(new TableColumn($data['Field']));
    }
  }

  /**
   * ManagedTable::getColumn()
   *
   * @param mixed $name
   * @return
   */
  public function getColumn($name)
  {
    if (isset($this->tableColumns[$name])) {
      return $this->tableColumns[$name]; 
    } else {
      $this->error("Column '$name' not found");
    }
  }

  /**
   * ManagedTable::joinTable()
   *
   * @param mixed $joinedTable
   * @return
   */
  public function joinTable(JoinedTable $joinedTable)
  {
    $this->mysqlJoinedTables[] = new JoinedTable($table, $constraint);
  }

  /**
   * ManagedTable::addButton()
   *
   * @param mixed $tableButton
   * @return
   */
  public function addButton(TableButton $tableButton)
  {
    $this->tableButtons[] = $tableButton;
  }

  /**
   * ManagedTable::addMassOperator()
   *
   * @param mixed $massOperator
   * @return
   */
  public function addMassOperator(MassOperator $massOperator)
  {
    $this->massOperators[] = $massOperator;
  }

  /**
   * ManagedTable::addColumn()
   *
   * @param mixed $tableColumn
   * @return
   */
  public function addColumn(TableColumn $tableColumn)
  {
    $this->tableColumns[$tableColumn->name] = $tableColumn;
    $this->tableColumnNames[] = $tableColumn->name;
  }

  /**
   * ManagedTable::setDefaultSortColumn()
   *
   * @param mixed $sortColumn
   * @return
   */
  public function setDefaultSortColumn($sortColumn)
  {
    if (in_array($sortColumn, $this->tableColumnNames)) {
      $this->defaultSortColumn = $sortColumn;
    } else {
      $this->error('Invalid column specified for sorting');
    }
  }

  /**
   * ManagedTable::setDefaultSortDirection()
   *
   * @param mixed $sortDirection
   * @return
   */
  public function setDefaultSortDirection($sortDirection)
  {
    if (in_array($sortDirection, $this->validSortDirections)) {
      $this->defaultSortDirection = $sortDirection;
    } else {
      $this->error('Invalid sort direction specified');
    }
  }

  public function setDefaultMaxRows($maxRows)
  {
    if (in_array($maxRows, $this->maxRowsOptions)) {
      $this->defaultMaxRows = $maxRows;
    } else {
      $this->error('Invalid value specified for default max rows');
    }
  }

  /**
   * ManagedTable::setDefaultSearchColumn()
   *
   * @param mixed $searchColumn
   * @return
   */
  public function setDefaultSearchColumn($searchColumn)
  {
    if (in_array($searchColumn, $this->tableColumnNames)) {
      $this->defaultSearchColumn = $searchColumn;
    } else {
      $this->error('Invalid default search column specified');
    }
  }

  /**
   * ManagedTable::setMaxButtons()
   *
   * @param mixed $numButtons
   * @return void
   */
  public function setMaxButtons($numButtons)
  {
    if (is_numeric($numButtons)) {
      $this->maxButtons = $numButtons;
    } else {
      $this->error('Invalid value specified for maximum number of buttons');
    }
  }

  /**
   * ManagedTable::addConstraint()
   *
   * Adds a constraint to the mysql query.
   *
   * @param mixed $constraint
   * @return
   */
  public function addConstraint($constraint)
  {
    $this->mysqlConstraints[] = $constraint;
  }

  public function enableMassOperators($rowKey)
  {
    if (in_array($rowKey, $this->tableColumnNames)) {
      $this->massOperatorsEnabled = true;
      $this->massOperatorsKey = $rowKey;
    } else {
      $this->error('Invalid column specified for mass operator key');
    }
  }

  /**
   * ManagedTable::show()
   *
   * @return
   */
  public function show()
  {
    if (count($this->tableColumns) == 0) {
      $this->error('No columns selected');
    }
    $this->validateInput();
    $this->queryMySQL();

    /* If there are no results, we don't need buttons */
    if ($this->resultSize == 0) {
      $this->tableButtons = array();
    }

    /* If there isn't more than one row, no need for the massoperator */
    if ($this->resultSize < 2) {
      $this->massOperatorsEnabled = false;
    }

    /* 
      Calculate how wide the left margin needs to be to fit in buttons and the
      mass operator checkboxes.
    */
    $buttonMultiplier = (count($this->tableButtons) > $this->maxButtons) ? 1 :
        count($this->tableButtons);
    $this->leftMarginWidth = self::LEFT_MARGIN + self::BUTTON_WIDTH *
        $buttonMultiplier;
    if ($this->massOperatorsEnabled) {
      $this->leftMarginWidth += self::BUTTON_WIDTH;
    }

    echo "<div class=\"table-container\" " .
        "style=\"margin-left: {$this->leftMarginWidth}px\">";
    $this->showSearchBar();

    echo "<table cellpadding=\"0\" cellspacing=\"0\" " .
        "class=\"table-table\">\n";
    $this->showTableHeader();
    $this->showTableData();
    echo "</table>\n";

    if ($this->resultSize == 0) {
      echo "<div class=\"table-noresults\">No records were found.</div>";
    } else {
      $this->showFooter();
    }
    echo "</div><!-- End ManagedTable -->\n";
  }

  /**
   * ManagedTable::error()
   *
   * For now this is just an alias for die(). May want to expand this in the
   * future.   
   *
   * @param mixed $message
   * @return void
   */
  final protected function error($message)
  {
    die($message);
  }

  final protected function debugPrint($message)
  {
    if ($this->debug) {
      echo "$message\n";
    }
  }

  /**
   * ManagedTable::validateInput()
   *
   * Set various class properties from data in the $_GET superglobal. Validate
   * any data retreived.
   *
   * @return
   */
  protected function validateInput()
  {
    if (isset($_GET[self::PARAM_MAX_ROWS]) &&
        in_array($_GET[self::PARAM_MAX_ROWS], $this->maxRowsOptions)) {
      $this->maxRows = Util::getMysqlSafeString($_GET[self::PARAM_MAX_ROWS]);
    } elseif (isset($this->defaultMaxRows)) {
      $this->maxRows = $this->defaultMaxRows;
    }

    if (isset($_GET[self::PARAM_PAGE_NUMBER]) &&
        is_numeric($_GET[self::PARAM_PAGE_NUMBER])) {
      $this->pageNumber = Util::getMysqlSafeString(
          $_GET[self::PARAM_PAGE_NUMBER]);
    }

    if (isset($_GET[self::PARAM_SORT_DIR]) &&
        (in_array($_GET[self::PARAM_SORT_DIR], $this->validSortDirections))) {
      $this->sortDirection = Util::getMysqlSafeString(
          $_GET[self::PARAM_SORT_DIR]);
    } elseif (isset($this->defaultSortDirection)) {
      $this->sortDirection = $this->defaultSortDirection;
    }

    if (isset($_GET[self::PARAM_SORT_COL]) &&
        (in_array($_GET[self::PARAM_SORT_COL], $this->tableColumnNames))) {
      $this->sortColumn = Util::getMysqlSafeString($_GET[self::PARAM_SORT_COL]);
    } elseif (isset($this->defaultSortColumn)) {
      $this->sortColumn = $this->defaultSortColumn;
    }

    if (isset($_GET[self::PARAM_SEARCH_KEY]) &&
        in_array($_GET[self::PARAM_SEARCH_KEY], $this->tableColumnNames)) {
      $this->searchKey = Util::getMysqlSafeString(
          $_GET[self::PARAM_SEARCH_KEY]);
    } elseif (isset($this->defaultSearchColumn)) {
      $this->searchKey = $this->defaultSearchColumn;
    }

    if (isset($_GET[self::PARAM_SEARCH_VAL])) {
      $this->searchValue = Util::getMysqlSafeString(
          $_GET[self::PARAM_SEARCH_VAL]);
    }

    if (isset($_GET[self::PARAM_SEARCH_MODE]) &&
        (in_array($_GET[self::PARAM_SEARCH_MODE], $this->validSearchModes))) {
      $this->searchMode = Util::getMysqlSafeString(
          $_GET[self::PARAM_SEARCH_MODE]);
    }

    $this->mysqlStart = max($this->maxRows * ($this->pageNumber - 1), 0);
  }

  /**
   * ManagedTable::queryMySQL()
   *
   * @return
   */
  protected function queryMySQL()
  {
    $mysqlConstraints = implode(" AND ", $this->mysqlConstraints);
    $validSearch = isset($this->searchKey) && ($this->searchValue != '') &&
        ($this->searchEnabled);

    if (isset($this->mysqlDatabase)) {
      $from = $this->mysqlDatabase . '.' . $this->mysqlTable;
    } else {
      $from = $this->mysqlTable;
    }

    $query1 = 'SELECT ' . $this->getFieldList() . " FROM $from";
    $query2 = 'SELECT COUNT(DISTINCT ' . $this->mysqlPrimaryKey .
        ") AS total FROM $from";
    $queryBoth = '';

    foreach ($this->mysqlJoinedTables as $join) {
      $queryBoth .= " LEFT JOIN " . $join['table'] . " ON " .
          $join['constraint'];
    }

    if ((count($this->mysqlConstraints) > 0) || ($validSearch)) {
      $queryBoth .= " WHERE ";
    }
    if (count($this->mysqlConstraints) > 0) {
      $queryBoth .= $mysqlConstraints;
    }
    if ((count($this->mysqlConstraints) > 0) && ($validSearch)) {
      $queryBoth .= " AND ";
    }

    if ($validSearch) {
      if ($this->searchMode == self::SEARCH_EXACT) {
        $queryBoth .= $this->searchKey . " = '" . $this->searchValue . "'";
      } elseif ($this->searchMode == self::SEARCH_GROUP) {
        $s = str_replace(',', "','", $this->searchValue);
        $queryBoth .= $this->searchKey . " IN ('$s')";
      } else {
        $queryBoth .= $this->searchKey . " LIKE '%" . $this->searchValue . "%'";
      }
    }
    if (isset($this->mysqlGroupBy)) {
      $queryBoth .= " GROUP BY " . $this->mysqlGroupBy;
    }
    $query1 .= $queryBoth;
    $query2 .= $queryBoth;

    if (isset($this->sortColumn)) {
      $query1 .= " ORDER BY `" . $this->sortColumn . "` " .
          $this->sortDirection;
    }
    $query1 .= " LIMIT " . $this->mysqlStart . ", " . $this->maxRows;

    $this->debugPrint("<br><br>MAIN QUERY: " . $query1 . "<br><br>");

    $result = Util::query($query1);

    $this->debugPrint("<br><br>COUNT QUERY: " . $query2 . "<br><br>");

    $count = Util::query($query2);

    $this->resultSize = mysql_result($count, 0);
    while ($data = mysql_fetch_array($result, MYSQL_ASSOC)) {
      $this->tableData[] = $data;
    }
  }

  /**
   * ManagedTable::getFieldList()
   *
   * Returns a comma-delimited list of columns in the table. This is used to
   * generate the field list in the mysql query.
   *
   * @return
   */
  protected function getFieldList()
  {
    $temp = array();
    foreach ($this->tableColumns as $column) {
      if (isset($column->source)) {
        array_push($temp, "`$column->name`" . ' AS ' . "`$column->source`");
      } else {
        array_push($temp, "`$column->name`");
      }
    }
    return implode(', ', $temp);
  }

  /**
   * ManagedTable::buildUrl()
   *
   * This is used to make links on the page without losing any data from the
   * current $_GET string. It takes in an associative array of key/value pairs
   * and then merges them with the current query string and returns a new url.
   *
   * @param mixed $keyValuePairs
   * @return A URL with the supplied key/value pairs merged with $_GET.
   */
  protected function buildUrl($keyValuePairs)
  {
    $query = array();
    foreach ($_GET as $key => $value) {
      $query[$key] = $value;
    }
    foreach ($keyValuePairs as $key => $value) {
      $query[$key] = $value;
    }
    return $_SERVER['PHP_SELF'] . '?' . http_build_query($query, '', '&amp;');
  }

  /**
   * ManagedTable::showSearchBar()
   *
   * Displays the search bar at the top of the form.
   *
   * @return
   */
  protected function showSearchBar()
  {
    echo "<div class=\"table-searchbar\">\n";

    echo "<form id=\"table-form\" method=\"get\" action=\"" .
        $_SERVER['PHP_SELF'] . "\" class=\"table-searchbar-form\">\n";

    /* Search fields */
    echo "<div class=\"table-searchbar-fields\">";
    echo "Search: <select name=\"" . self::PARAM_SEARCH_KEY . "\">\n";
    foreach ($this->tableColumns as $column) {
      if ($column->isSearchable()) {
        $selected = ($column->name == $this->searchKey) ?
            ' selected="selected"' : '';
        echo "<option value=\"$column->name\"$selected>$column->title" .
            "</option>\n";
      }
    }
    echo "</select>\n";
    echo "For: <input type=\"text\" id=\"table-searchbox\" name=\"" .
        self::PARAM_SEARCH_VAL . "\" value=\"$this->searchValue\" />\n";
    echo "Type: <select name=\"" . self::PARAM_SEARCH_MODE . "\">\n";
    $searchTypes = array('Normal' => self::SEARCH_NORMAL,
        'Exact' => self::SEARCH_EXACT, 'Group' => self::SEARCH_GROUP);
    foreach ($searchTypes as $title => $value) {
      $selected = ($value == $this->searchMode) ? ' selected="selected"' : '';
      echo "<option value=\"$value\"$selected>$title</option>\n";
    }
    echo "</select>\n";
    echo "</div>";

    /* Search and Reset buttons */
    echo "<div class=\"table-searchbar-buttons\">";
    echo "<div class=\"table-button-medium\">" .
        "<a href=\"#\" onclick=\"table.submitSearchForm()\">Search</a>" .
        "</div>\n";
    echo "<div class=\"table-button-medium\">" .
        "<a href=\"{$_SERVER['PHP_SELF']}\">Reset</a></div>\n";
    /* Record the max rows so it stays the same when searching */
    echo "<input type=\"hidden\" value=\"$this->maxRows\" name=\"" .
        self::PARAM_MAX_ROWS . "\" />\n";
    echo "</div>";

    echo "</form>\n";

    /* Rows per page dropdown menu */
    echo "<div class=\"table-searchbar-rows\"># / Page:\n";
    echo "<select onchange=\"table.changeRowMax(this.value)\">\n";
    foreach ($this->maxRowsOptions as $maxRows) {
      $selected = ($maxRows == $this->maxRows) ? ' selected="selected"' : '';
      $url = $this->buildUrl(array(self::PARAM_MAX_ROWS => $maxRows,
          self::PARAM_PAGE_NUMBER => NULL));
      echo "<option value=\"$url\"$selected>$maxRows</option>\n";
    }
    echo "</select>\n";
    echo "</div>\n";
    echo "</div>\n";
  }

  /**
   * ManagedTable::showHeader()
   * 
   * Shows the table header.
   *
   * @return
   */
  protected function showTableHeader()
  {
    echo "<tr class=\"table-header\">\n";
    $firstCell = true;
    foreach ($this->tableColumns as $column) {
      if ($column->isHidden()) {
        continue;
      }
      if ($this->sortEnabled) {
        if ($this->sortColumn == $column->name) {
          if ($this->sortDirection == self::SORT_ASCENDING) {
            $sortDirection = self::SORT_DESCENDING;
            $image = "<img src=\"" . HTTP_COMMON .
                "images/arrow-down.gif\" alt=\"Down Arrow\" />";
          } else {
            $sortDirection = self::SORT_ASCENDING;
            $image = "<img src=\"" . HTTP_COMMON .
                "images/arrow-up.gif\" alt=\"Up Arrow\" />";
          } 
        } else {
          $sortDirection = self::SORT_ASCENDING;
          $image = '';
        }
        $url = $this->buildUrl(array(self::PARAM_SORT_COL => $column->name,
                                     self::PARAM_SORT_DIR => $sortDirection));
        echo "<td>";
        if ($firstCell) {
          $this->showCheckboxToggle();
          $firstCell = false;
        }
        echo "<a href=\"$url\">$column->title</a> $image";
        echo "</td>\n";
      } else {
        echo "<td>$column->title</td>\n";
      }
    }
    echo "</tr>\n";
  }

  /**
   * ManagedTable::showCheckBoxToggle()
   *
   * @return void
   */
  protected function showCheckBoxToggle()
  {
    if ($this->massOperatorsEnabled) {
      $style = "width: {$this->leftMarginWidth}px; " .
          "left: -{$this->leftMarginWidth}px";
      echo "<div class=\"table-buttons-container\">\n";
      echo "<div class=\"table-buttons\" style=\"$style\">\n";
      echo "<img onclick=\"table.toggleCheckBoxes()\" src=\"" .
          HTTP_COMMON . "images/button-small-checkbox.gif\" alt=\"Toggle\"" .
          "title=\"Toggle Check Boxes\" />";
      echo "</div>\n";
      echo "</div>\n";
    }
  }

  /**
   * ManagedTable::showData()
   *
   * Shows the data portion of the table.  
   *
   * @return
   */
  protected function showTableData()
  {
    foreach ($this->tableData as $rowNum => $tableRow) {
      $class = (1 & $rowNum) ? 'table-evenrows' : 'table-oddrows';
      echo "<tr class=\"$class\">\n";
      $firstCell = true;
      foreach ($tableRow as $cellName => $cellValue) {
        $column = $this->tableColumns[$cellName];
        if ($column->isHidden()) {
          continue;
        }
        echo "<td>";
        if ($firstCell) {
          $this->showButtons($tableRow);
          $firstCell = false;  
        }
        echo $column->getHTML($cellValue, $tableRow);
        echo "</td>\n";
      }
      echo "</tr>\n";
    }
  }

  /**
   * ManagedTable::showButtons()
   *
   * Show buttons for a table row. Note that this uses an absolutely positioned 
   * div in the first table cell of the row which is then positioned into the
   * left margin of the table. It's a little complicated, but it's the only way
   * to get the nice border effect.
   *
   * @param mixed $tableRow
   * @return
   */
  protected function showButtons($tableRow)
  {
    echo "<div class=\"table-buttons-container\">\n";
    $x = $this->leftMarginWidth;
    echo "<div class=\"table-buttons\" " .
        "style=\"width: {$x}px; left: -{$x}px\">\n";
    if ($this->massOperatorsEnabled) {
      $this->showCheckbox($tableRow);
    }
    if (count($this->tableButtons) > $this->maxButtons) {
      $buttons = array();
      foreach ($this->tableButtons as $button) {
        $buttons[] = $button->getJavaScript($tableRow);
      }
      $onclick = 'table.showButtonDropMenu(this, {' .
          implode(', ', $buttons) . '})';
      echo "<div class=\"table-button-dropmenu\" " .
          "onclick=\"$onclick\"></div>\n";
    } else {
      foreach ($this->tableButtons as $button) {
        echo "<div class=\"table-button\">";
        echo $button->getHTML($tableRow);
        echo "</div>\n";
      }
    }
    echo "</div>\n";
    echo "</div>\n";
  }

  /**
   * ManagedTable::showCheckbox()
   *
   * @param mixed $tableRow
   * @return void
   */
  protected function showCheckbox($tableRow)
  {
    $value = $tableRow[$this->massOperatorsKey];
    echo "<div class=\"table-checkbox\">\n";
    echo "<input type=\"checkbox\" name=\"table-checkboxes\" " .
        "value=\"$value\" />\n";
    echo "</div>\n";
  }

  /**
   * ManagedTable::showFooter()
   *
   * @return void
   */
  protected function showFooter()
  {
    if ($this->massOperatorsEnabled || $this->resultSize > $this->maxRows) {
      echo "<div class=\"table-footer\">\n";
      if ($this->massOperatorsEnabled) {
        $this->showMassOperator();
      }
      if ($this->resultSize > $this->maxRows) {
        $this->showPagination();
      }
      echo "<div class=\"clear-floats\"></div>\n";
      echo "</div>\n";
    }
  }

  /**
   * ManagedTable::showMassOperator()
   *
   * @return
   */
  protected function showMassOperator()
  {
    // TODO: This will need to be updated, I don't feel like doing it right now.
    echo "<div class=\"table-mass-operator\">\n";
    echo "<select id=\"table-mass-operator\" " .
        "onchange=\"table.massOperate(this.value)\">\n";
    echo "<option>With Selected:</option>\n";
    foreach ($this->massOperators as $operator) {
      $value = $operator->getUrl();
      $option = $operator->getName();
      echo "<option value=\"$value\">$option</option>\n";
    }
    echo "</select>\n";
    echo "</div>\n";
  }

  /**
   * ManagedTable::showPagination()
   * 
   * Calculates and shows pagination links below the table.  
   *
   * @return
   */
  protected function showPagination()
  {
    $numPages = ceil($this->resultSize / $this->maxRows);
    $firstPage = 1;
    $previousPage = $this->pageNumber - 1;
    $nextPage = $this->pageNumber + 1;
    $lastPage = $numPages;

    $pageNumbers = array($this->pageNumber);
    $pagesBefore = range($this->pageNumber - self::PAGINATION_LINKS,
        $this->pageNumber - 1);
    $pagesAfter = range($this->pageNumber + 1, $this->pageNumber +
        self::PAGINATION_LINKS);
    while (count($pageNumbers) < self::PAGINATION_LINKS) {
      $addedSomething = false;
      $value = array_pop($pagesBefore);
      if ($value != NULL && $value > 0) {
        array_unshift($pageNumbers, $value);
        $addedSomething = true;
      }
      $value = array_shift($pagesAfter);
      if ($value != NULL && $value <= $numPages) { 
        array_push($pageNumbers, $value);
        $addedSomething = true;
      }
      if (!$addedSomething) break;
    }

    echo "<div class=\"table-pagination\">\n";

    /* 'First' Link */
    if ($firstPage != $this->pageNumber) {
      $url = $this->buildUrl(array(self::PARAM_PAGE_NUMBER => $firstPage));
      echo "<a href=\"$url\">First</a> |\n"; 
    } else {
      echo "First |\n"; 
    }

    /* 'Previous' Link */
    if ($previousPage != $this->pageNumber && $previousPage > 0) {
      $url = $this->buildUrl(array(self::PARAM_PAGE_NUMBER => $previousPage));
      echo "<a href=\"$url\">Previous</a> |\n";
    } else {
      echo "Previous |\n";
    }

    if ($pageNumbers[0] != $firstPage) {
      echo " ... ";
    }

    /* Numbered Links */
    foreach ($pageNumbers as $pageNumber) {
      if ($pageNumber != $this->pageNumber) {
        $url = $this->buildUrl(array(self::PARAM_PAGE_NUMBER => $pageNumber));
        echo " <a href=\"$url\">$pageNumber</a> \n"; 
      } else {
        echo " <strong>$pageNumber</strong> \n"; 
      }
    }

    if ($pageNumbers[count($pageNumbers) - 1] != $lastPage) {
      echo " ... ";
    }

    /* 'Next' Link */
    if ($nextPage != $this->pageNumber && $nextPage <= $numPages) {
      $url = $this->buildUrl(array(self::PARAM_PAGE_NUMBER => $nextPage));
      echo "| <a href=\"$url\">Next</a>\n"; 
    } else {
      echo "| Next\n"; 
    }

    /* 'Last' Link */
    if ($lastPage != $this->pageNumber) {
      $url = $this->buildUrl(array(self::PARAM_PAGE_NUMBER => $lastPage));
      echo "| <a href=\"$url\">Last</a>\n"; 
    } else {
      echo "| Last\n"; 
    }

    echo "</div>\n";
  }
}


/**
 * TableColumn
 *
 * @package 
 * @author Michael Thomas
 * @copyright 2008
 * @version $Id$
 * @access public
 */
class TableColumn
{
  const FORMAT_LINK = 1;
  const FORMAT_TRUNCATED = 2;
  const FORMAT_DATE = 3;
  const FORMAT_CUSTOM = 4;

  public $name;
  public $title;
  public $asKey;

  protected $style;
  protected $searchable = true;
  protected $hidden = false;

  protected $format = 0;
  protected $linkText;
  protected $truncateLength;
  protected $truncateEllipsis;
  protected $dateFormat;
  protected $customFunction;

  /**
   * TableColumn::TableColumn()
   *
   * If you are joining a table with the same column names, you can use the
   * $source parameter to specify exactly what field in what table to use. Note
   * that the column name must be unique.
   *
   * For example: 'SELECT table1.col1 AS table1_col1' is accomplished with
   *
   *   new TableColumn('table1_col1', 'Title', 'table1.col1');    
   *
   * @param mixed $name The unique name of this column in the table.
   * @param mixed $title The title to display at the top of the column.
   * @param mixed $source The optional source of the column if the name is not
   *     unique.
   * @return void
   */
  public function __construct($name, $title = null, $source = null)
  {
    $this->name = $name;
    $this->title = ($title != null) ? $title :
        ucwords(str_replace('_', ' ', $name));
    $this->source = $source;
  }

  /**
   * TableColumn::setTitle()
   *
   * @param mixed $title
   * @return void
   */
  public function setTitle($title)
  {
    $this->title = $title;
  }

  /**
   * TableColumn::setSearchable()
   *
   * @param mixed $searchable
   * @return
   */
  public function setSearchable($searchable)
  {
    if (is_bool($searchable)) {
      $this->searchable = $searchable;
    } else {
      $this->error('setSearchable: boolean expected');  
    }
  }

  /**
   * TableColumn::setHidden()
   *
   * @param mixed $hidden
   * @return
   */
  public function setHidden($hidden)
  {
    if (is_bool($hidden)) {
      $this->hidden = $hidden;
      if ($this->hidden) {
        $this->searchable = false;
      }
    } else {
      $this->error('setHidden: boolean expected');  
    }
  }

  /**
   * TableColumn::setStyle()
   *
   * @param mixed $style
   * @return
   */
  public function setStyle($style)
  {
    $this->style = $style;
  }

  /**
   * TableColumn::isSearchable()
   *
   * @return
   */
  public function isSearchable()
  {
    return $this->searchable;
  }

  /**
   * TableColumn::isHidden()
   *
   * @return
   */
  public function isHidden()
  {
    return $this->hidden;
  }

  /**
   * TableColumn::getHTML()
   *
   * @param mixed $cellData
   * @param mixed $rowData
   * @return
   */
  public function getHTML($cellData, $rowData)
  {
    switch ($this->format) {
      case self::FORMAT_CUSTOM:
        $html = $this->applyFormatCustom($rowData);
        break;
      case self::FORMAT_DATE:
        $html = $this->applyFormatDate($cellData);
        break;
      case self::FORMAT_LINK:
        $html = $this->applyFormatLink($cellData);
        break;
      case self::FORMAT_TRUNCATED:
        $html = $this->applyFormatTruncated($cellData);
        break;
      default:
        $html = $cellData;
        break;
    }
    return (isset($this->style)) ? "<span style=\"$this->style\">$html</span>" :
        $html;
  }

  /**
   * TableColumn::setFormatLink()
   *
   * @param mixed $linkText
   * @param string $linkTitle
   * @return
   */
  public function setFormatLink($linkText, $linkTitle = '')
  {
    $this->format = self::FORMAT_LINK;
    $this->linkText = $linkText;
    $this->linkTitle = $linkTitle;
  }

  /**
   * TableColumn::setFormatTruncated()
   *
   * @param mixed $length
   * @param bool $ellipsis
   * @return
   */
  public function setFormatTruncated($length, $ellipsis = true)
  {
    $this->format = self::FORMAT_TRUNCATED;
    $this->truncateLength = $length;
    $this->truncateEllipsis = $ellipsis;
  }

  /**
   * TableColumn::setFormatDate()
   *
   * @param string $formatString
   * @return
   */
  public function setFormatDate($formatString = '%Y-%m-%d')
  {
    $this->format = self::FORMAT_DATE;
    $this->dateFormat = $formatString;
  }

  /**
   * TableColumn::setFormatCustom()
   *
   * @param mixed $functionName
   * @return
   */
  public function setFormatCustom($functionName)
  {
    $this->format = self::FORMAT_CUSTOM;
    $this->customFunction = $functionName;
  }

  /**
   * TableColumn::applyFormatLink()
   *
   * @param mixed $cellData
   * @return
   */
  protected function applyFormatLink($cellData)
  {
    if (strlen($cellData) > 0) {
      return "<a href=\"$cellData\" " .
          "title=\"$this->linkTitle\">$this->linkText</a>";
    } else {
      return $cellData;
    }
  }

  /**
   * TableColumn::applyFormatTruncated()
   *
   * @param mixed $cellData
   * @return
   */
  protected function applyFormatTruncated($cellData)
  {
    $stripped = strip_tags($cellData);
    $string = substr($stripped, 0, $this->truncateLength);
    if ($this->truncateEllipsis && strlen($stripped) > $this->truncateLength) {
      return "<span title=\"$stripped\">$string...</span>";
    } elseif (strlen($stripped) > $this->truncateLength) {
      return "<span title=\"$stripped\">$string</span>";
    } else {
      return "$string";
    }
  }

  /**
   * TableColumn::applyFormatDate()
   *
   * @param mixed $cellData
   * @return
   */
  protected function applyFormatDate($cellData)
  {
    $unixTime = (is_numeric($cellData)) ? $cellData : strtotime($cellData);
    return strftime($this->dateFormat, $unixTime);
  }

  /**
   * TableColumn::applyFormatCustom()
   *
   * @param mixed $rowData
   * @return
   */
  protected function applyFormatCustom($rowData)
  {
    return call_user_func($this->customFunction, $rowData);
  }

}

/**
 * TableButton
 *
 * @package 
 * @author Michael Thomas
 * @copyright 2008
 * @version $Id$
 * @access private
 */
class TableButton
{
  public $url;
  public $rowKey;
  public $name;
  public $getParameter;
  public $type;

  /**
   * TableButton::TableButton()
   *
   * @param mixed $name
   * @param mixed $url
   * @param mixed $rowKey
   * @param mixed $getParameter
   * @return
   */
  public function __construct($name, $url, $rowKey, $getParameter)
  {
    $this->name = $name;
    $this->url = $url;
    $this->rowKey = $rowKey;
    $this->getParameter = $getParameter;
  }

  /**
   * TableButton::getHTML()
   *
   * @param mixed $tableRow
   * @return
   */
  public function getHTML($tableRow)
  {
    $separator = (strpos($this->url, '?') !== false) ? '&' : '?'; 
    $url = $this->url . $separator . $this->getParameter . '=' .
        $tableRow[$this->rowKey];
    return "<a href=\"$url\">$this->name</a>";
  }

  /**
   * TableButton::getJavaScript()
   *
   * @param mixed $tableRow
   * @return
   */
  public function getJavaScript($tableRow)
  {
    $separator = (strpos($this->url, '?') !== false) ? '&' : '?'; 
    $url = $this->url . $separator . $this->getParameter . '=' .
        $tableRow[$this->rowKey];
    return "'$this->name' : '$url'";
  }
}

/**
 * JoinedTable
 *
 * @package 
 * @author Michael Thomas
 * @copyright 2008
 * @version $Id$
 * @access private
 */
class JoinedTable
{
  public $table;
  public $constraint;

  /**
   * JoinedTable::__construct()
   *
   * @param mixed $table
   * @param mixed $constraint
   * @return
   */
  public function __construct($table, $constraint)
  {
    $this->table = $table;
    $this->constraint = $constraint;
  }
}

/**
 * MassOperator
 *
 * @package 
 * @author 
 * @copyright 2008
 * @version $Id$
 * @access private
 */
class MassOperator
{
  protected $name;
  protected $url;
  protected $getParameter;

  /**
   * MassOperator::__construct()
   *
   * @param mixed $name
   * @param mixed $url
   * @param mixed $getParameter
   * @return
   */
  public function __construct($name, $url, $getParameter)
  {
    $this->name = $name;
    $this->url = $url;
    $this->getParameter = $getParameter;
  }

  /**
   * MassOperator::getUrl()
   *
   * @return
   */
  public function getUrl()
  {
    $separator = (strpos($this->url, '?') !== false) ? '&' : '?'; 
    $url = $this->url . $separator . $this->getParameter . '='; 
    return $url;
  }

  /**
   * MassOperator::getName()
   *
   * @return
   */
  public function getName()
  {
    return $this->name;
  }
}

?>